#include "stdafx.h"
#include "Lexecutioner.h"
#include <fstream>
#include <cstdlib>
#include <algorithm>
#include <functional>
#include <iterator>
#include <set>
#include <map>
#include <oleauto.h>
#include "../Lexer/LexerProxy.h"
#include "Support.h"
#include <cctype>

// called in flexlexer but not implemented
// just forward to our assert inducing version below
int yyFlexLexer::yylex()
{
    return static_cast<MiniBasic::Lexecutioner*>(this)->yylex();
}

namespace MiniBasic
{
    namespace
    {
        // magic signature for minibasic files
        const mbint MB_SIGNATURE = 987654321;
        // maximum line number
        const mbint MB_MAXLINENO = 99999;
        // maximum size of lines
        const mbint MB_MAXLINECHARS = 255;
    }

    Lexecutioner::Lexecutioner(BSTR file) : mbFile(0)
    {
        HRESULT hr = PrepareFile(file);
        if(FAILED(hr))
        {
            throw hr;
        }
        linePosition = linesAndOffsets.begin();
    }

    Lexecutioner::Lexecutioner(const char* code, SIZE_T length, bool textLineNumbers) : mbFile(length)
    {
        std::string codeBuf(code, code + length);
        std::istringstream codeInMemory(codeBuf);
        HRESULT hr = PrepareCode(codeInMemory, length, textLineNumbers);
        if(FAILED(hr))
        {
            throw hr;
        }
        linePosition = linesAndOffsets.begin();
    }

    Lexecutioner::filePosition Lexecutioner::GetCurrentLine() const
    {
        // if the last token read was a "newline" then the lexer has already
        // moved to the next line, however we're being called in the context
        // of a completed sequence of tokens from the previous line
        // so we return info for the previous line so accurate info for diagnostics
        // and for/gosub tracking purposes is used

        // another hack around Flex, this time it's const correction
        char lastChar = *(const_cast<Lexecutioner*>(this)->YYText());
        if(lastChar == '\n')
        {
            Lexecutioner::filePosition pos = linePosition;
            return --pos;
        }
        else
        {
            return linePosition;
        }
    }

    ULONG_PTR Lexecutioner::GetCurrentFilePosition(bool movePastIfCurrentNewLine)
    {
        // the current file position is the current position of
        // the mbFile file pointer, minus the size left in 
        // the lexers internal buffer
        LARGE_INTEGER li = {0};
        ULARGE_INTEGER ulNewPos;
        if(SUCCEEDED(mbFile.Seek(li, STREAM_SEEK_CUR, &ulNewPos)))
        {
            const char* currentBufferPos = YYText();
            if(movePastIfCurrentNewLine)
            {
                if(*currentBufferPos == '\n')
                {
                    ++currentBufferPos;
                }
            }
            ULONG_PTR leftInBuffer = GetCharsLeftInBuffer(currentBufferPos, yy_current_buffer);
            return static_cast<ULONG_PTR>(ulNewPos.QuadPart) - leftInBuffer;
        }
        else
        {
            return MAXULONG_PTR;
        }
    }

    bool Lexecutioner::LineExists(mbint line) const
    {
        const filePosition pos = linesAndOffsets.find(line);
        return pos != linesAndOffsets.end();
    }

    // should seek is set to true when reading a comment
    // so the file pointer is updated
    void Lexecutioner::GotoNextLine(bool shouldSeek)
    {
        ++linePosition;
        seenAssign = false;
        if(shouldSeek)
        {
            ULONG_PTR offset = 0;
            if(linePosition == linesAndOffsets.end())
            {
                offset = mbFile.Size();
            }
            else offset = linePosition->second;
            MoveToFilePosition(offset, false);
        }
    }

    void Lexecutioner::SeenAssign(bool hasSeenAssign)
    {
        seenAssign = hasSeenAssign;
    }

    bool Lexecutioner::SeenAssign() const
    {
        return seenAssign;
    }

    void Lexecutioner::MoveToLine(mbint line)
    {
        filePosition newPos = linesAndOffsets.find(line);
        assert(newPos != linesAndOffsets.end());
        linePosition = newPos;
        MoveToFilePosition(newPos->second, false);
    }

    void Lexecutioner::MoveToFilePosition(ULONG_PTR fileOffset, bool adjustFilePosition)
    {
        if(adjustFilePosition)
        {
            // if we have to adjust the line position
            // we must first find which line the offset is in
            //filePosition newLinePos = 
            assert(!linesAndOffsets.empty());
            filePosition next = linesAndOffsets.begin();
            filePosition lineIter = next++;
            const filePosition lineEnd = --linesAndOffsets.end();
            for(; lineIter != lineEnd; lineIter = next++)
            {
                if(lineIter->second <= fileOffset && fileOffset < next->second) break;
            }
            linePosition = lineIter;
        }
        LARGE_INTEGER offset = {fileOffset};
        HRESULT hr = mbFile.Seek(offset, STREAM_SEEK_SET, NULL);
        assert(hr == S_OK);
        yyrestart(0);
        seenAssign = false;
        UNREFERENCED_PARAMETER(hr);
    }

    // defined as pure virtual so we need an implementation
    // shouldn't ever get called as out lexer uses the two argument
    // version of yylex
    int Lexecutioner::yylex()
    {
        assert("Shouldn't be called");
        return 0;
    }

    // custom input routine called by yylex
    int Lexecutioner::LexerInput(char* outputBuffer, int max_size)
    {
        ULONG read = 0;
        mbFile.Read(outputBuffer, max_size, &read);
        return read;
    }

    void Lexecutioner::PutBackLastToken()
    {
        // in the current implementation we need to do nothing
        // except to move the mbFile pointer to the current lexer position
        ULONG_PTR currentPos = GetCurrentFilePosition();
        MoveToFilePosition(currentPos, true);
        //currentPos -= YYLeng();
        //const char* text = YYText();
    }

    HRESULT Lexecutioner::PrepareFile(BSTR filename)
    {
        HRESULT hr = S_OK;
        std::ifstream file(filename, std::ios::binary);
        if(file)
        {
            mbint magic = 0;
            // get the magic header out the way
            file.read(reinterpret_cast<char*>(&magic), sizeof(magic));
            WIN32_FILE_ATTRIBUTE_DATA wfad = {0};
            if(GetFileAttributesEx(filename, GetFileExInfoStandard, &wfad))
            {
                // given the constraints set forth in the manual
                // a 4GB minibasic file is impossible, the theoretical largest is 
                // 99999 statements * 255 chars per statement = 25.4 MB
                // but since the parameters are configurable
                // we retain this for correctness
                SIZE_T fileSize = 
#if _WIN64
                    (static_cast<ULONGLONG>(wfad.nFileSizeHigh) << 32 | wfad.nFileSizeLow);
#else
                    wfad.nFileSizeLow;
#endif
                std::string fileInMemory((std::istreambuf_iterator<char>(file)), (std::istreambuf_iterator<char>()));
                std::istringstream code(fileInMemory);
                hr = PrepareCode(code, fileSize, false);
            }
        }
        return hr;
    }

    HRESULT Lexecutioner::PrepareCode(std::istringstream& codeInMemory, SIZE_T size, bool hasTextLineNumbers)
    {
        HRESULT hr = S_OK;
        ULARGE_INTEGER memSize = {size};
        if(SUCCEEDED(hr = mbFile.SetSize(memSize)))
        {
            ULONG_PTR offsetIntoMemoryArea = 0;
            std::string temp; // so we don't destroy and reallocate for every line
            while(codeInMemory)
            {
                mbint line;
                void* result;
                if(!hasTextLineNumbers)
                {
                    result = codeInMemory.read(reinterpret_cast<char*>(&line), sizeof(line));
                }
                else
                {
                    result = codeInMemory >> line;
                }
                if(result)
                {
                    // remove leading spaces from the line
                    // not using isspace as that would go past \r
                    while(codeInMemory.peek() == ' ' || codeInMemory.peek() == '\t')
                    {
                        char dummy;
                        codeInMemory.read(&dummy, 1);
                    }
                    // lines terminated by \r
                    if(std::getline(codeInMemory, temp, '\r'))
                    {
                        // put a newline on the back of the string
                        temp.push_back('\n');
                        linesAndOffsets[line] = offsetIntoMemoryArea;
                        ULONG written = 0;
                        if(SUCCEEDED(hr = mbFile.Write(temp.c_str(), temp.size(), &written)))
                        {
                            offsetIntoMemoryArea += written;
                        }
                        else break;
                    }
                }
            }
            // trim the memory to size
            memSize.QuadPart = offsetIntoMemoryArea;
            hr = mbFile.SetSize(memSize);
        }
        if(SUCCEEDED(hr))
        {
            // we've written to the end of the mapping
            // so reset the pointer to the beginning
            LARGE_INTEGER li = {0};
            hr = mbFile.Seek(li, STREAM_SEEK_SET, NULL);
        }
        return hr;
    }

    HRESULT Lexecutioner::CheckFileValidity(BSTR filename)
    {
        HRESULT hr = S_OK;
        bool valid = false;        
        std::ifstream file(filename, std::ios::binary);
        if(file)
        {
            mbint magic = 0;
            file.read(reinterpret_cast<char*>(&magic), sizeof(magic));
            if(magic == MB_SIGNATURE)
            {
                std::string temp; // so we don't destroy and reallocate for every line
                std::set<mbint> lineNumbers;
                mbint highLineNum = 0;
                while(file)
                {
                    mbint line;
                    if(file.read(reinterpret_cast<char*>(&line), sizeof(line)))
                    {
                        // ensure the line numbers always increase
                        if(line > highLineNum)
                        {
                            highLineNum = line;
                            // see if it's a unique number
                            std::pair<std::set<mbint>::iterator, bool> insertRet = lineNumbers.insert(line);
                            if(!insertRet.second) break;
                            // discard the rest of the line
                            // seems they're are terminated by \r
                            if(std::getline(file, temp, '\r'))
                            {
                                if(temp.size() > MB_MAXLINECHARS) 
                                {
                                    break;
                                }
                            }
                            else
                            {
                                if(file.eof())
                                {
                                    hr = HRESULT_FROM_WIN32(ERROR_HANDLE_EOF);
                                }
                                else hr = HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
                                break;
                            }
                        }
                        else break;
                    }
                }
                valid = file.eof() && (!lineNumbers.empty()) && (*(lineNumbers.rbegin()) <= MB_MAXLINENO);
            }
        }
        return FAILED(hr) ? hr : (valid ? S_OK : HRESULT_FROM_WIN32(ERROR_BAD_FORMAT));
    }
}